feat: add Swarm permission manifests - #168
Conversation
Discover and validate bzz-hosted capability manifests before privileged provider requests. Project approved grants with provenance, recovery, lifecycle pruning, consent UI, and Settings controls.
Queue manifest, permission, and feed mutations per origin to close cross-window races. Add unresolved-fetch backoff and exhaustive recovery coverage across journal and authority-store write boundaries.
Extract the interoperable discovery, schema, capability, consent lifecycle, and security contract from the browser implementation. Include the detailed design as non-normative rationale and call out the versioning boundary SwarmID must coordinate around.
Document fail-closed behavior for unresolved tracked manifests and add an informative provider-method mapping. Distinguish base-connection operations from capability-gated methods so the profile matches the reference implementation.
# Conflicts: # src/main/index.js # src/main/preload.test.js # src/main/swarm/feed-store.js # src/renderer/lib/wallet/permission-manage.js
|
Rebuilt on
Full swarm/permission suites pass; the only failing unit tests in my bare worktree are the known missing-native-dep ones (@ledgerhq/@ghostery, installed in CI) + the vault flake. |
|
[alan-review R1] Blocking — app fails to launch: duplicate
Empirically confirmed on this branch ( Every harness e2e spec fails with Fix: delete line 316. Note this also blocked any visual verification of the manifest consent UI in this round — please re-run a harness spec after the fix. |
|
[alan-review R1] Blocking — Every sibling swarm prompt ( Concrete failure — two tabs on different manifest-bearing bzz apps, each issues a non-public swarm method, sheet #1 is on screen when consent #2 arrives:
Related sibling gap in the same file: the queue-based prompts also pre-check Fix suggestion: route the sheet through (Verified by code trace against |
|
[alan-review R1] Minor findings (combined):
|
|
[alan-verify R1] Both findings verified against R1-F1 — CONFIRMED (blocking). No window ever opens; R1-F2 — CONFIRMED (blocking). Sheet as it renders today (captured with line 316 removed locally so the app can start) — in the concurrent case it looks identical but is inert: Side note for the fix round, not a new finding: Working tree left clean; throwaway repro/capture files deleted. |
…consents registerSwarmProviderIpc() was called twice in bootstrap(); ipcMain.handle throws on the second registration for swarm:provider-execute, so bootstrap rejected before createMainWindow() and the app never opened a window. showPermissionManifest() kept a single overwritable `pending` slot, so two concurrent consent requests clobbered each other: the first hung forever, the second was auto-denied by its own hideAllSubscreens() transition, and the sheet stayed on screen with dead buttons. Move the sibling prompts' createPromptQueue into a shared prompt-queue.js and use it for the manifest sheet too — concurrent consents queue, dismissal denies all of them, and claim() gives the same double-click protection as the other prompts.
|
[alan-fix R1] Both confirmed findings fixed in R1-F1 — duplicate R1-F2 — manifest consent used a single overwritable
New unit tests in Verification
Acceptance evidence — request 1 on screen (captured inside the input-protection window, hence the dimmed Allow all), then request 2 taking the screen with live buttons after request 1 is allowed: |
|
[alan-review R2] Blocking — concurrent same-origin consents mint independent tokens; after the first decide, the second Allow throws `Manifest consent is stale` and fails the page's request ( The R1-F2 queue fix makes concurrent manifest consents sequential in the UI, but main still mints a fresh token per Concrete failure — user opens the same manifest-bearing bzz app in two tabs; both pages call
The deny-then-allow variant is worse for intent: user denies in tab A (first-contact record deleted), then answers sheet B either way → same stale throw, so tab B's answer is never honored. Empirically confirmed against the real module (jest repro with the store on a temp dir, mocked bzz fetch — same harness as The renderer leg (second identical sheet presented from the queue) is exactly the flow shown in the R1 fix round's Fix suggestion: reuse an outstanding token for the same |
|
[alan-review R2] Minor findings (combined):
Note: the four R1 minor findings (prune-on- |
|
[alan-verify R2] R2-F1 — CONFIRMED. Two same-origin Reproduced deterministically against the real module (temp jest spec, since deleted), both bare and wrapped in Visually confirmed too (harness e2e, temp spec since deleted): two concurrent Refutation attempts that failed: the per-origin mutex only orders the calls; |
Two tabs of the same manifest-bearing app both call swarm_requestAccess on load. Each check minted its own token off the same baseRevision, so the user was shown two identical sheets and the second decide() failed the revision guard the first decision had just bumped — the second tab's request rejected with "Manifest consent is stale" despite the user having consented (the first-contact deny/allow variant tripped the fingerprint guard the same way). checkManifest() now hands back the outstanding token when an undecided consent for the same origin, manifest fingerprint, base revision, and capability set already exists, so the second decide() replays the recorded result instead of throwing. `firstContact` is deliberately excluded from that match: the first check persists its observation, so a sibling check sees a record and computes false — keeping the outstanding token's value is what still drops the observation on a denial. The sheet coalesces on that shared token too, so the second tab rides on the one answer rather than re-asking an identical question (and a late arrival on an already-answered token replays it without a sheet).
|
[alan-fix R2] — fixed in R2-F1 — concurrent same-origin consents mint independent tokens (blocking) Root cause: Fix — one outstanding consent per (origin, manifest fingerprint, base
Nothing was loosened in the staleness guards: a manifest that actually changed Tests: 3 new main-process cases (shared token + both decisions succeed; shared Verified in the real app (harness Electron run: real main-process Before (at After ( …and one "Allow all" settles both tabs — sheet gone, no duplicate queued |
|
[alan-review R3] Blocking — a transient network error mid-body during manifest fetch is classified
Concrete failure: user has answered "Allow all" for a manifest-bearing app (managed connection, auto-approve publish/feeds, feed grant, app-scoped identity). On a later navigation the page issues any non-public swarm method → Empirically confirmed against the real module (temp jest spec using the same harness as Fix suggestion: in |
|
[alan-review R3] Minor findings (combined):
Verification notes for this round: full swarm/manifest unit suites (7 suites, 133 tests) pass on |
|
[alan-verify R3] R3-F1 ( Tried to refute it three ways and it survived all three:
Deterministic repro (throwaway jest spec, run and deleted — repo left clean). Same origin, managed
So a transient hiccup that lands one packet later than the one the |
A manifest fetch that returned 200 and then died mid-body (bee restart,
dropped socket) surfaced from discover() as {status:'invalid'} — the same
verdict as a malformed manifest — because readLimitedJson's reader.read()
rejection landed in the same catch as JSON.parse and validateManifest
failures. checkManifest then ran pruneRecord for the origin: connection
revoked, feed grant revoked, auto-approvals cleared, record deleted. A
transient hiccup the unresolved backoff exists to absorb destroyed the
user's manifest-managed authority.
Tag read failures as transport errors and classify them 'unresolved' so
they feed the backoff. Only bytes we hold and cannot accept — oversized,
non-JSON, schema-violating — stay 'invalid'. Also cancel the reader on
the mid-body failure path.
|
[alan-fix R3] — fixed in R3-F1 (blocking, Root cause: Fix: split byte-reading out into Tests (both fail without the fix, verified by reverting the one-line classification):
Existing Checked the sibling manual-body-reader sites for the same misclassification: Verification: full unit suite No screenshot: this change has no UI surface — it alters how a backend fetch failure is classified, and the network-free |
|
[alan-review R1] (fresh review pass at
Previously-raised items still open at Verification this round: re-reviewed the R3 fix — the |
…ch guard - outstandingToken reuse now extends the shared consent's expiresAt, so a second tab coalescing onto a token minted 4.5 min into the 5 min TTL no longer inherits a window that dies 30 s later. Test added (fake timers). - Move the normalizeOrigin(committedUrl) !== key mismatch guard above the prune branch so origin A's record can never be pruned on origin B's manifest state (defense-in-depth; unreachable from today's renderer).
|
Fresh alan review confirms Fixed the 2 fresh R1 minors in
The tracked backlog minors (individual-grant journaling, fingerprint |
Six tracked follow-ups from earlier review rounds, all previously non-blocking: - `individual` decisions journal their connection grant through runTransaction instead of granting ahead of it, so a crash between the two no longer leaves an origin connected with nothing to replay. - Receipts attest the rawHash of the bytes their sheet was built from (carried on the consent token) rather than the latest observation, so a wording-only redeploy during an outstanding consent can no longer pair new bytes with the wording the user actually saw. The semantic fingerprint stays schema + capability keys per design §6.1. - feed-store reports user mutations (feed grant, active publisher identity) to the manifest layer like swarm-permissions already does, so a hand-made change detaches manifest ownership instead of leaving a record that claims — and later re-asserts — the flag. Manifest projections pass `source: 'manifest'`; IPC forces `source: 'user'`. - The manifest consent sheet pre-checks isSignatureInFlight() like its siblings; the messaging prompt, which was missing the same guard, gets it too. - checkManifest's no-mutation branch re-reads the store after the discover() await instead of saving the pre-await snapshot. - The manifest body read has a 15s inactivity deadline, so a gateway that sends headers then stalls is aborted and classified `unresolved` instead of hanging every non-public swarm call for the origin. Tests: each fix has a unit test that fails against the pre-fix module.
|
[alan-fix R4] The six tracked backlog minors are fixed in earlier-R1 minor 2 — earlier-R1 minor 3 — fingerprint vs. The actual harm the finding named — "the receipt then attests text that isn't in the manifest it hashes" — is real and is fixed at its source: the consent token now carries the earlier-R1 minor 4 — feed-store / identity user mutations don't detach ( R2 minor 1 — signature-in-flight pre-check on the manifest sheet ( R2 minor 2 — R3 minor 1 (remaining half) — no deadline on the body read Verification
Acceptance evidence — the sheet refuses to open over the device prompt ( Working tree left clean; throwaway spec deleted. With this the tracked backlog is empty — earlier-R1 minor 1 (prune on |
|
[alan-review R1] (fresh loop, reviewing at
Verification this round:
Working tree left clean; throwaway spec and test artifacts deleted. |










Summary
freedom-manifest.jsonfor committedbzz:applicationsWhy
Swarm applications currently request connection and operation permissions incrementally. A co-deployed manifest lets the browser present one understandable permission decision while binding automatic grants to the capability set the application continues to declare. Provenance and lifecycle pruning ensure manual overrides remain user-owned and removed capabilities do not retain manifest-managed authority.
This PR targets
feature/window-swarm-enhancementsbecause messaging is part of the capability registry and projection model.Validation
npm run lintnpm test— 112 suites and 2,247 tests passed; 10 existing skipsnpm run build -- --mac --arm64 --unsigned